using System;
using System.Collections.Generic;
using System.Data.SqlTypes;
using System.Drawing;
using System.Globalization;
using System.Linq;
using System.Threading;
using log4net.Repository.Hierarchy;
using Microsoft.SqlServer.Types;
using SinBiota.Model.Business.MapServices.Datatypes;
using SinBiota.Model.Business.MapServices.Interfaces;

namespace SinBiota.Model.Business.MapServices.Implementation
{
    public class SpatialServices : ISpatialServices
    {
/*
        private const double EarthRadius = 6378137;
*/
        private const double MinLatitude = -85.05112878;
        private const double MaxLatitude = 85.05112878;
        private const double MinLongitude = -180;
        private const double MaxLongitude = 180;


        /// <summary>
        /// Converts a point from latitude/longitude WGS-84 coordinates (in degrees)
        /// into pixel XY coordinates at a specified level of detail.
        /// </summary>
        /// <param name="latitude">Latitude of the point, in degrees.</param>
        /// <param name="longitude">Longitude of the point, in degrees.</param>
        /// <param name="levelOfDetail">Level of detail, from 1 (lowest detail)
        /// to 23 (highest detail).</param>
        /// <param name="tileImageSize"></param>
        public Point LatLongToPixel(double latitude, double longitude, int levelOfDetail, int tileImageSize)
        {
            latitude = Clip(latitude, MinLatitude, MaxLatitude);
            longitude = Clip(longitude, MinLongitude, MaxLongitude);

            var x = (longitude + 180) / 360;
            var sinLatitude = Math.Sin(latitude * Math.PI / 180);
            var y = 0.5 - Math.Log((1 + sinLatitude) / (1 - sinLatitude)) / (4 * Math.PI);

            var mapSize1 = tileImageSize * MapTileSize(levelOfDetail);
            return new Point((int) Clip(x*mapSize1 + 0.5, 0, mapSize1 - 1),
                             (int) Clip(y*mapSize1 + 0.5, 0, mapSize1 - 1));
        }

        public LatLong TileXYToLatLong(int tileX, int tileY, int zoom) 
        {
            double tileSize = MapTileSize(zoom);
            var x = tileX/ tileSize - 0.5;
            var y = 0.5 - tileY/tileSize;
            return new LatLong(90 - 360 * Math.Atan(Math.Exp(-y * 2 * Math.PI)) / Math.PI, 360 * x);
        }

        public IList<SqlGeography> GetGeometriesInsideQuadkey(IList<SqlGeography> geographyList, string quadkey)
        {
            // Zoomlevel:
            var zoomLevel = quadkey.Length;
            //Get TileXY-Coordinates
            var tileXY = QuadKeyToTileXY(quadkey);
            //Get Latitude and Longitude of the Corners
            var nwLatLong = TileXYToLatLong(tileXY.X, tileXY.Y, zoomLevel);
            var seLatLong = TileXYToLatLong(tileXY.X + 1, tileXY.Y + 1, zoomLevel);
            // Create new geography with this coordinates:
            var nwLon = nwLatLong.Longitude;
            var nwLat = nwLatLong.Latitude;
            var seLat = seLatLong.Latitude;
            var seLon = seLatLong.Longitude;
            var polygon = string.Format(CultureInfo.InvariantCulture.NumberFormat, "POLYGON(({0} {1}, {2} {3}, {4} {5}, {6} {7}, {8} {9}))", nwLon, nwLat, nwLon, seLat, seLon, seLat, seLon, nwLat, nwLon, nwLat);
            var boundingBox = SqlGeography.STPolyFromText(new SqlChars(polygon), 4326);
            // Create new List:
            return geographyList.Where(g => boundingBox.STIntersects(g).IsTrue).ToList();
        }

        public IList<SqlGeography> GetGeometriesIntersectionQuadkey(IList<SqlGeography> geographyList, string quadkey)
        {
            // Zoomlevel:
            var zoomLevel = quadkey.Length;
            //Get TileXY-Coordinates
            var tileXY = QuadKeyToTileXY(quadkey);
            //Get Latitude and Longitude of the Corners, 
            var nwLatLong = TileXYToLatLong(tileXY.X, tileXY.Y, zoomLevel);
            var seLatLong = TileXYToLatLong(tileXY.X + 1, tileXY.Y + 1, zoomLevel);
            // Create new geography with this coordinates:
            var nwLon = nwLatLong.Longitude;
            var nwLat = nwLatLong.Latitude;
            var seLat = seLatLong.Latitude;
            var seLon = seLatLong.Longitude;
            var polygon = string.Format(CultureInfo.InvariantCulture.NumberFormat, "POLYGON(({0} {1}, {2} {3}, {4} {5}, {6} {7}, {8} {9}))", nwLon, nwLat, nwLon, seLat, seLon, seLat, seLon, nwLat, nwLon, nwLat);
            var boundingBox = SqlGeography.STPolyFromText(new SqlChars(polygon), 4326);
            // Create new List:
            return geographyList.Select(boundingBox.STIntersection).Where(intersection => intersection.STIsEmpty().IsFalse).ToList();
        }

        /// <summary>
        /// Converts a QuadKey into tile XY coordinates.
        /// </summary>
        /// <param name="quadKey">QuadKey of the tile.</param>
        public Point QuadKeyToTileXY(string quadKey)
        {
            var tileX = 0;
            var tileY = 0;
            var levelOfDetail = quadKey.Length;
            for (var i = levelOfDetail; i > 0; i--)
            {
                var mask = 1 << (i - 1);
                switch (quadKey[levelOfDetail - i])
                {
                    case '0':
                        break;
                    case '1':
                        tileX = tileX | mask;
                        break;
                    case '2':
                        tileY = tileY | mask;
                        break;
                    case '3':
                        tileX = tileX | mask;
                        tileY = tileY | mask;
                        break;
                    default:
                        throw new ArgumentException("Invalid QuadKey digit sequence.");
                }
            }
            return new Point(tileX, tileY);
        }

        /// <summary>
        /// Clips a number to the specified minimum and maximum values.
        /// </summary>
        /// <param name="n">The number to clip.</param>
        /// <param name="minValue">Minimum allowable value.</param>
        /// <param name="maxValue">Maximum allowable value.</param>
        /// <returns>The clipped value.</returns>
        private static double Clip(double n, double minValue, double maxValue)
        {
            return Math.Min(Math.Max(n, minValue), maxValue);
        }

        /// <summary>
        /// Determines the map width and height (in tiles) at a specified level
        /// of detail.
        /// </summary>
        /// <param name="zoom">Level of detail, from 1 (lowest detail)
        /// to 23 (highest detail).</param>
        /// <returns>The map width and height in tiles.</returns>
        private static uint MapTileSize(int zoom)
        {
            return (uint)1 << zoom;
        }
    }
}